Skip to content

PERF: avoid repeated text fetch preparation for fetchmany(1) - #891

Merged
Jahnvi Thakkar (jahnvi480) merged 6 commits into
mainfrom
jahnvi/single-row-fetch-optimization
Nov 13, 2026
Merged

Jahnvi Thakkar (jahnvi480) merged 6 commits into
mainfrom
jahnvi/single-row-fetch-optimization

Conversation

@jahnvi480

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

N/A

GitHub Issue: #554


Summary

Keep size-one text fetches on native completion so ordinary row-count updates do not rebuild the dispatch policy for every row. Bounded wide text reuses the established batch decoder; narrow text retains the batch reader. Preserve Row/RowMapping behavior, converter bytes, malformed-text handling, mutation checks and consumed-row completion. No prefetching, weak-state cache or stack-buffer changes.

Add exactly one PR performance workload: fetchmany_one_text, collecting 10,000 ordered INT/NVARCHAR rows with explicit fetchmany(1). It retains rows through EOF and validates values/types outside timing. Existing comparison thresholds and bulk controls are unchanged. Add one workload/report contract test and update the registry/documentation.

Correct the GC-sensitive plain-tuple count test after reproducing its original failure: collection of an older watched cursor during plan preparation legitimately invalidates the global epoch. Keep exact stable-path counts and separately test the required one-read fallback; production invalidation is unchanged.

Validation

  • python -m pre_commit run black-check --all-files --hook-stage pre-push: passed.
  • /fetchstudy/venv/bin/python /fetchstudy/validate_worker.py candidate-on tests: passed, 145 report contract tests. The import-safe runner executes pytest.main(["-q", "-ra", "--tb=short", "tests/test_036_profiler_ci.py"]) against the fresh candidate build.
  • New workload: one warmup pair and five fresh-process alternating measured pairs on private SQL Server 2022, Linux x86_64 / CPython 3.13, matching profiling-enabled Release builds. Main/candidate medians were 1767.565 / 22.495 ms; policy preparations were 10,001 / 2 and Python fallbacks 10,001 / 1 in every measured sample. These instrumented timings are not release-latency or pyodbc comparisons.
  • The unchanged driver patch also passed focused OFF 643 / ON 651 tests, a separate fault-build gate of 281 tests, and 1195 common OFF tests. Original failures and expected-negative baseline tests are retained in the investigation evidence.

Draft / remaining performance qualification

Measured base is 730cb2bec05c650b1dba069bf9cbbadb810291d9. Later main 01a18f76a58ccf8f23e2c3d3f88b88a37b81ac54 changes only the parameter helper and its tests; it was not folded into local measurements. CI must validate the current merge.

The separate profiling-OFF study demonstrates the mixed-text regression improvement but does not beat pyodbc or show repeatable fetchone gains. Adverse bulk-fetch controls remain unresolved: the second primary fetchall comparison is candidate/main 1.221, 95% CI [1.060, 1.410]; fetchmany(1000) is also adverse. Identical-binary replicas vary under shared-host contention, but this does not establish that the regressions are harmless. Keep this PR draft pending clean control measurements and cross-platform CI; no regression-free or merge-ready claim.

Preserve batch decoding while completing eligible size-one text fetches natively. Add one mixed-text performance-report workload and focused contract coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 12, 2026 07:45
@github-actions github-actions Bot added the pr-size: medium Moderate update size label Nov 12, 2026
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

PR Performance Report

✅ Performance improved

1 database task consistently improved across 2 measured environments. No consistent slowdowns were detected.

1 IMPROVEMENT 0 SLOWDOWNS 2/2 ENVIRONMENTS

Signal fingerprint

Database task Unix / SQL Server 2022 Unix / SQL Server 2025
10,000 INT/NVARCHAR rows / fetchmany(1) 98.2% faster 97.7% faster

Coverage: 2 of 2 environments completed. Advisory result; does not block merging.

Measured timings
Environment Database task Before After Change
Unix / SQL Server 2022 10,000 INT/NVARCHAR rows / fetchmany(1) 1355.798 ms 24.454 ms -98.2%
Unix / SQL Server 2025 10,000 INT/NVARCHAR rows / fetchmany(1) 1133.470 ms 26.256 ms -97.7%
Performance diagnostics

Phase times are inclusive diagnostics and must not be added together. They identify where measured time changed, not why it changed.

Unix / SQL Server 2022

10,000 INT/NVARCHAR rows / fetchmany(1): ddbc::NativeCursor::prepare -1111.036 ms; ddbc::NativeCursor::fallback -225.222 ms; py::fetchmany::cpp_call -65.007 ms. Call changes: ddbc::FetchBatchData (added, removed, or intermittent); ddbc::FetchBatchData::SQLFetchScroll_call (added, removed, or intermittent); ddbc::FetchBatchData::cache_column_metadata (added, removed, or intermittent).

Unix / SQL Server 2025

10,000 INT/NVARCHAR rows / fetchmany(1): ddbc::NativeCursor::prepare -968.076 ms; ddbc::NativeCursor::fallback -148.066 ms; py::fetchmany::cpp_call -41.414 ms. Call changes: ddbc::FetchBatchData (added, removed, or intermittent); ddbc::FetchBatchData::SQLFetchScroll_call (added, removed, or intermittent); ddbc::FetchBatchData::cache_column_metadata (added, removed, or intermittent).

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 10.274 ms 10.255 ms -0.2% no signal
SELECT queries 1.000 ms 1.028 ms +0.5% no signal
Row insertion 30.276 ms 30.387 ms +0.4% no signal
Executemany inserts 158.876 ms 163.194 ms +2.5% no signal
Fetch-all queries 117.895 ms 119.160 ms +1.1% no signal
Row-by-row fetching 4.837 ms 4.901 ms +0.8% no signal
Batched row fetching 116.274 ms 115.906 ms -0.3% no signal
Transaction commit and rollback 105.058 ms 105.113 ms +0.1% no signal
Arrow row fetching 88.171 ms 87.753 ms +0.8% no signal
100,000-row insertion 362.280 ms 359.230 ms +1.6% no signal
Row fetching in batches of 100 120.447 ms 119.801 ms -0.2% no signal
Row fetching in batches of 10,000 139.998 ms 129.504 ms -0.9% no signal
Repeated positional queries 31.954 ms 31.978 ms -0.1% no signal
Repeated named-parameter queries 32.275 ms 32.353 ms +0.2% no signal
Legacy 100,000-row insertion 258.502 ms 260.853 ms +0.2% no signal
Insertion with explicit input sizes 395.826 ms 402.522 ms +1.2% no signal
Joined aggregation queries 180.035 ms 181.876 ms +1.0% no signal
Large joined-result fetching 196.102 ms 195.471 ms +1.1% no signal
1.2-million-row fetching 3468.496 ms 3438.596 ms -0.9% no signal
Common table expression queries 5.372 ms 5.376 ms +2.5% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.652 ms 1.529 ms -7.5% no signal
10,000 scalar values / fetchval() (debug disabled) 16.653 ms 16.703 ms -1.7% no signal
10,000 output-converted rows / fetchall() 24.005 ms 19.270 ms -20.0% no signal
10,000 output-converted rows / fetchmany(1000) 24.578 ms 23.354 ms -5.0% no signal
10,000 INT/NVARCHAR rows / fetchmany(1) 1355.798 ms 24.454 ms -98.2% consistent improvement
10,000 rows x 32 columns / dict(row._mapping) (warm) 31.651 ms 32.989 ms +4.2% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 80.384 ms 80.831 ms +0.5% no signal
SELECT queries 0.971 ms 0.984 ms +1.5% no signal
Row insertion 29.036 ms 29.082 ms +0.0% no signal
Executemany inserts 133.071 ms 132.251 ms +0.7% no signal
Fetch-all queries 132.329 ms 116.543 ms -8.2% no signal
Row-by-row fetching 4.706 ms 4.659 ms -1.9% no signal
Batched row fetching 109.593 ms 109.170 ms -0.2% no signal
Transaction commit and rollback 99.644 ms 99.617 ms +0.1% no signal
Arrow row fetching 82.501 ms 84.206 ms +1.5% no signal
100,000-row insertion 323.181 ms 347.794 ms +5.4% no signal
Row fetching in batches of 100 106.579 ms 105.478 ms -1.8% no signal
Row fetching in batches of 10,000 117.716 ms 129.892 ms +8.5% no signal
Repeated positional queries 29.543 ms 29.505 ms -0.0% no signal
Repeated named-parameter queries 29.830 ms 29.862 ms +0.5% no signal
Legacy 100,000-row insertion 228.706 ms 230.640 ms -0.0% no signal
Insertion with explicit input sizes 348.846 ms 352.570 ms +2.0% no signal
Joined aggregation queries 170.023 ms 172.710 ms +1.6% no signal
Large joined-result fetching 187.453 ms 182.255 ms -2.8% no signal
1.2-million-row fetching 3478.377 ms 3480.432 ms +0.3% no signal
Common table expression queries 5.211 ms 5.248 ms +1.1% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.402 ms 1.368 ms -2.1% no signal
10,000 scalar values / fetchval() (debug disabled) 16.617 ms 16.441 ms -1.0% no signal
10,000 output-converted rows / fetchall() 18.987 ms 18.461 ms -2.7% no signal
10,000 output-converted rows / fetchmany(1000) 23.244 ms 23.277 ms -0.2% no signal
10,000 INT/NVARCHAR rows / fetchmany(1) 1133.470 ms 26.256 ms -97.7% consistent improvement
10,000 rows x 32 columns / dict(row._mapping) (warm) 31.648 ms 32.854 ms +3.8% no signal
Build and measurement details

ADO build 191899

PR head: cda2d2eb62293d3fa38e97f958081f7e1723cfb6
Base: c4b17b2f4846e4fbf887091afae022b51ed82894
Measured merge: d83ba93cc9d1f2fa74ae69e64e8141b4ffcfd862

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4325.4; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5065.9; 5 paired comparisons and 1 warmup.

A consistent change requires more than 20% median paired movement, at least 1 ms between the median runtimes, and at least 80% of pairs exceeding the relative threshold in the same direction. A slowdown without enough pair agreement is reported as inconsistent.

The displayed change is the median of paired before-and-after ratios. It is not recalculated from the two displayed median runtimes.

Both revisions use profiling-enabled builds on the same agent and database, with alternating order and discarded warmups. Results are diagnostic and do not represent production-wheel latency.

Raw samples and logs are attached to the ADO run as profiler-* artifacts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Source-only review cannot establish cross-platform/live-SQL correctness of the native fetch changes or attribute the reported profiling-OFF bulk-fetch regressions without clean controls.

No open issues 🎉

What changed in this PR

This PR targets repeated dispatch preparation for text-bearing fetchmany(1) calls in the native driver, addressing part of the fetch-performance concerns raised in #554.

Changes:

  • Keep size-one text fetches on native completion, sharing batch decoding for bounded wide text and retaining the batch reader for narrow text.
  • Add regression coverage for dispatch counts, text boundaries, converter bytes, malformed text, and garbage-collection invalidation.
  • Add one 10,000-row INT/NVARCHAR workload with timing-boundary and report-contract tests, bringing the profiler registry to 26 tasks.

The reported instrumented gains do not establish release-latency improvements. Bulk-fetch regressions remain unresolved, and the PR remains draft pending clean control measurements and cross-platform validation.

File Description
tests/​test_single_row_pipeline.py Tests wide-text boundaries and updated native-path counts.
tests/​test_output_converter_bytes.py Extends converter and malformed-text coverage to fetchmany(1).
tests/​test_native_fetch_dispatch.py Tests stable dispatch reuse and garbage-collection invalidation.
tests/​test_036_profiler_ci.py Validates the new workload’s timing, retention, cleanup, and reporting.
mssql_python/​pybind/​native_fetch.hpp Completes size-one text reads natively without repeated policy preparation.
mssql_python/​pybind/​fetch_text.hpp Extracts the shared batch UTF-16 decoder.
mssql_python/​pybind/​ddbc_bindings.h Uses the shared decoder for bound wide-text batches.
mssql_python/​pybind/​ddbc_bindings.cpp Enables bounded wide-text single-row reads with batch decoding semantics.
mssql_python/​_native_fetch.py Updates dispatch documentation for size-one text fetches.
eng/​profiler_benchmarks/​workloads.py Adds and registers fetchmany_one_text.
eng/​profiler_benchmarks/​report.py Adds the new workload’s report label.
eng/​profiler_benchmarks/​README.md Documents workload boundaries and updates registry counts.

Review effort: Balanced

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

86%


🎯 Overall Coverage

86%


📈 Total Lines Covered: 11566 out of 13381
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/pybind/ddbc_bindings.cpp (50.0%): Missing lines 3556,3943-3945
  • mssql_python/pybind/ddbc_bindings.h (100%)
  • mssql_python/pybind/fetch_text.hpp (100%)
  • mssql_python/pybind/native_fetch.hpp (94.4%): Missing lines 741

Summary

  • Total: 37 lines
  • Missing: 5 lines
  • Coverage: 86%

mssql_python/pybind/ddbc_bindings.cpp

Lines 3552-3560

  3552             (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE));
  3553 }
  3554 
  3555 static inline bool CanUseSingleRowForFetchMany(SQLSMALLINT dataType, SQLULEN columnSize) {
! 3556     // Narrow text still needs the batch's platform/codec-specific conversion.
  3557     switch (dataType) {
  3558         case SQL_CHAR:
  3559         case SQL_VARCHAR:
  3560         case SQL_LONGVARCHAR:

Lines 3939-3949

  3939                                     text = OutputConverters::text(textData, textSize);
  3940                                 row.append(text ? text
  3941                                                 : batchText
  3942                                                       ? steal(FetchText::decode_batch_utf16_native(
! 3943                                                             textData, textSize))
! 3944                                                       : FetchText::from_utf16_native(textData,
! 3945                                                                                      textSize));
  3946                                 LOG("SQLGetData: Appended NVARCHAR string "
  3947                                     "length=%lu for column %d",
  3948                                     (unsigned long)numCharsInData, i);
  3949                             } else {

mssql_python/pybind/native_fetch.hpp

Lines 737-745

  737         ret = FetchMany_wrap(plan->handle, rows, 1, plan->charEncoding,
  738                              plan->wideEncoding, plan->charCtype, plan->at(Messages));
  739         if (!current(self, context, state, *plan, rt) ||
  740             (!SQL_SUCCEEDED(ret) && ret != SQL_NO_DATA))
! 741             return py::reinterpret_borrow<py::object>(self).attr("_complete_fetchmany")(rows, ret);
  742         if (PyList_GET_SIZE(rows.ptr()))
  743             values = py::reinterpret_borrow<py::list>(PyList_GET_ITEM(rows.ptr(), 0));
  744         else
  745             ret = SQL_NO_DATA;


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.performance_counter.hpp: 0.7%
mssql_python.pybind.logger_bridge.cpp: 57.9%
mssql_python.pybind.ddbc_bindings.h: 64.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python._native_fetch.py: 77.2%
mssql_python.__init__.py: 80.8%
mssql_python.pybind.row_mapping_cache.hpp: 81.5%
mssql_python.pybind.connection.connection_pool.cpp: 82.5%
mssql_python.pybind.ddbc_bindings.cpp: 82.5%
mssql_python.pybind.connection.connection.cpp: 83.1%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as ready for review November 12, 2026 08:07
Copilot AI balanced review requested due to automatic review settings November 12, 2026 08:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Native fetch routing and decoding require live SQL Server and cross-platform validation, while unresolved profiling-OFF bulk-fetch slowdowns need clean control measurements and human review.

No open issues 🎉

Review effort: Balanced

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the PR for correctness, security, reliability, performance, test coverage, repository conventions, and applicable architecture and design specifications. No actionable issues were identified. The implementation is consistent with repository standards and the applicable approved design requirements.

Recommendation: Approve

Copilot AI balanced review requested due to automatic review settings November 13, 2026 05:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Native fetch routing and platform-specific decoding need live SQL Server and cross-platform validation unavailable here, while unresolved profiling-OFF bulk-fetch slowdowns require clean control measurements and human review.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 13, 2026 07:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Reported profiling-OFF bulk-fetch slowdowns and unverified native routing and decoder parity require clean measurements, live SQL Server testing, and cross-platform CI results for human review.

No open issues 🎉

Review effort: Balanced

Collect prior watched cursor cycles before asserting stable native reenable counts. Extend deterministic GC coverage to converter callback removal, preserving exact fetch/fallback counts, metadata ownership, and callback semantics.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Native fetch routing and platform-specific decoding require unavailable live-SQL cross-platform validation, while reported profiling-OFF bulk-fetch regressions remain unresolved pending clean control measurements.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 13, 2026 11:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The CHAR/VARCHAR fallback lacks mutation-during-batch-fetch regression coverage because the existing test intercepts SQLFetch_ptr rather than SQLFetchScroll_ptr, leaving exactly-once processing and prevention of skipped or duplicated rows unverified.

1 open issue

Review effort: Balanced

Comment thread mssql_python/pybind/native_fetch.hpp Resolved
@jahnvi480
Jahnvi Thakkar (jahnvi480) merged commit 54237fd into main Nov 13, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: medium Moderate update size

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants